﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace NHINDirect
{   
    /// <summary>
    /// A set of prototype lightweight parsing routines. They utilize .NET's "lazy evaluation/LINQ" technique to generate
    /// a set of enumerators of the source - that can then be further composed into higher level constructs
    /// and integrated with LINQ, if required. 
    /// 
    /// A more FORMAL parser that includes a Lexer + explicitly follows the RFC2822 grammar and adheres to
    /// more classic Parser Patterns - will be written separately.
    /// 
    /// One key requirement for parsing is that underlying raw entity text is always retained VERBATIM. 
    /// For cryptography, it is important that post-parsing, we be able to recover the original entity AS IS.
    /// The parser therefore works exclusively with StringSegments - a lightweight struct NOT allocated from the Heap.
    /// StringSegment maintains offsets into the original entity text. 
    /// 
    /// This design has the added benefit of minimizing string allocations (substrings etc), since no strings are
    /// actually allocated. 
    /// 
    /// </summary>
    public static class EntityParser
    {
        public static T Read<T>(string entityText)
            where T : MimeEntity, new()
        {
            return EntityParser.Read<T>(new StringSegment(entityText));
        }
        
        public static T Read<T>(StringSegment entityText)
            where T : MimeEntity, new()
        {
            T entity = new T();
            foreach(EntityPart part in EntityParser.ReadEntityParts(entityText))
            {
                switch(part.Type)
                {
                    default:
                        break;
                        
                    case EntityPartType.Header:
                        entity.Headers.Add((Header) part);
                        break;
                    
                    case EntityPartType.Body:
                        if (entity.HasBody)
                        {
                            throw new ProtocolException(ProtocolError.InvalidBody);
                        }
                        entity.Body = (Body) part;
                        break;
                }
            }
            
            return entity;
        }
        
        public static IEnumerable<StringSegment> ReadHeaderParts(string source, char separator)
        {
            return ReadHeaderParts(new StringSegment(source), separator);
        }
        
        public static IEnumerable<StringSegment> ReadHeaderParts(StringSegment source, char separator)
        {
            int startAt = source.StartIndex;
            CharReader reader = new CharReader(source);
            while (reader.ReadTo(separator))
            {
                if (reader.IsPrev(Protocol.Escape))
                {
                    continue;
                }
                
                yield return new StringSegment(source.Source, startAt, reader.Position); // STRUCTS - fast
                startAt = reader.Position + 1;
            }            
            
            StringSegment last = new StringSegment(source.Source, startAt, reader.Position);
            if (!last.IsEmpty)
            {
                yield return last;
            }
        }
        
        public static IEnumerable<EntityPart> ReadBodyParts(string entity, string boundary)
        {
            return EntityParser.ReadBodyParts(new StringSegment(entity), boundary);
        }

        public static IEnumerable<EntityPart> ReadBodyParts(StringSegment entity, string boundary)
        {
            return EntityParser.ReadBodyParts(EntityParser.ReadLines(entity), boundary);
        }
        
        public static IEnumerable<EntityPart> ReadBodyParts(IEnumerable<StringSegment> bodyLines, string boundary)
        {
            if (bodyLines == null)
            {
                throw new ArgumentNullException();
            }
            
            IEnumerator<StringSegment> lineEnumerator = bodyLines.GetEnumerator();
            StringSegment part = StringSegment.Null;
            StringSegment prevLine = StringSegment.Null;
            EntityPartType expectedPart = EntityPartType.Prologue;
            
            //
            // As per the Multipart Spec:
            //  The boundary delimiter is actually CRLF--boundary
            // So we must maintain a stack (prevLine)..
            //            
            while (expectedPart != EntityPartType.Epilogue && lineEnumerator.MoveNext())
            {
                StringSegment line = lineEnumerator.Current;
                if (IsBoundary(line, boundary))
                {
                    if (!part.IsNull)
                    {
                        part.Union(prevLine);
                    }
                    //
                    // When we hit a boundary, we yield the part we've collected so far
                    //
                    EntityPartType partType = expectedPart;
                    switch(expectedPart)
                    {
                        default:
                            throw new ProtocolException(ProtocolError.InvalidBodySubpart);
                        
                        case EntityPartType.Prologue:
                            expectedPart = EntityPartType.SubPart;
                            break;
                        
                        case EntityPartType.SubPart:
                            if (IsBoundaryEnd(line))
                            {   
                                expectedPart = EntityPartType.Epilogue;
                            }
                            break;
                    }
                    
                    if (!part.IsNull)
                    {                    
                        yield return new EntityPart(partType, part);
                    }
                    
                    prevLine = StringSegment.Null;
                    part = StringSegment.Null;
                }
                else
                {
                    if (!prevLine.IsNull)
                    {
                        part.Union(prevLine);
                    }
                    
                    prevLine = line;
                }
            }
            
            if (expectedPart != EntityPartType.Epilogue)
            {
                throw new ProtocolException(ProtocolError.InvalidBodySubpart);
            }
            
            // Epilogue
            part = StringSegment.Null; 
            while (lineEnumerator.MoveNext())
            {
                part.Union(lineEnumerator.Current);
            }
            if (!part.IsEmpty)
            {
                yield return new EntityPart(EntityPartType.Epilogue, part);
            }
        }
        
        static bool IsBoundary(StringSegment line, string boundary)
        {
            int length = line.Length;
            return (length > 1
                    && length >= (boundary.Length + 2)
                    && line[0] == Protocol.BoundaryChar
                    && line[1] == Protocol.BoundaryChar 
                    && (string.Compare(boundary, 0, line.Source, line.StartIndex + 2, boundary.Length) == 0));
        }
        
        static bool IsBoundaryEnd(StringSegment line)
        {
            int length = line.Length;
            return (length > 4
                    && line[length - 1] == Protocol.BoundaryChar
                    && line[length - 2] == Protocol.BoundaryChar);
        }
        
        public static IEnumerable<EntityPart> ReadEntityParts(string entity)
        {
            return EntityParser.ReadEntityParts(new StringSegment(entity));
        }

        public static IEnumerable<EntityPart> ReadEntityParts(StringSegment entity)
        {
            return EntityParser.ReadEntityParts(EntityParser.ReadLines(entity));
        }
        
        public static IEnumerable<EntityPart> ReadEntityParts(IEnumerable<StringSegment> lines)
        {
            if (lines == null)
            {
                throw new ArgumentNullException();
            }
            
            EntityPartType expectedPartType = EntityPartType.Header;
            Header header = null;
            Body body = null;
            
            foreach(StringSegment line in lines)
            {
                switch(expectedPartType)
                {
                    default:
                        throw new ProtocolException(ProtocolError.Unexpected);
                        
                    case EntityPartType.Header:                        
                        if (line.IsEmpty)
                        {
                            if (header != null)
                            {
                                yield return header;
                                header = null;
                            }
                            
                            yield return new EntityPart(EntityPartType.HeaderBoundary, line);
                            //                            
                            // Done with the header section. Onto the Body
                            //
                            expectedPartType = EntityPartType.Body;
                            break;
                        }

                        if (Protocol.IsWhitespace(line[0]))
                        {
                            // Line folding. This line belongs to the current header
                            if (header == null)
                            {
                                throw new ProtocolException(ProtocolError.InvalidHeader);
                            }
                            header.AppendSourceText(line);
                            break;
                        }
                        
                        if (header != null)
                        {
                            yield return  header;
                        }
                        header = new Header(line);                        
                        break;
                    
                    case EntityPartType.Body:
                        if (body == null)
                        {
                            body = new Body(line);
                        }
                        else
                        {
                            body.AppendSourceText(line);
                        }
                        break;                        
                }
            }
            
            if (header != null)
            {
                yield return header;
            }
            if (body != null)
            {
                yield return body;
            }
        }
        
        public static IEnumerable<StringSegment> ReadLines(string entity)
        {
            return EntityParser.ReadLines(new StringSegment(entity));
        }
        
        public static IEnumerable<StringSegment> ReadLines(StringSegment entity)
        {
            CharReader reader = new CharReader(entity);
            int startIndex = reader.Position + 1;
            int endIndex = startIndex - 1;            
            char ch;
            
            while ((ch = reader.Read()) != char.MinValue)
            {
                switch (ch)
                {
                    default:
                        endIndex = reader.Position;
                        break;

                    case Protocol.CR:
                        //
                        // RFC 2822 mandates that CRLF be together always
                        //
                        if (reader.Read() != Protocol.LF)
                        {
                            throw new ProtocolException(ProtocolError.InvalidCRLF);
                        }
                        yield return new StringSegment(reader.Source, startIndex, endIndex);
                        
                        startIndex = reader.Position + 1;
                        endIndex = reader.Position;
                        break;
                        
                    case Protocol.LF:
                        //
                        // No standalone LF allowed
                        //
                        throw new ProtocolException(ProtocolError.InvalidCRLF);
                }
            }
        
            if (endIndex >= 0)
            {
                yield return new StringSegment(reader.Source, startIndex, endIndex);                
            }
        }
        
        public static StringSegment SkipWhitespace(StringSegment text)
        {
            CharReader reader = new CharReader(text); // Struct. Typically created on stack, so pretty efficient
            char ch;
            while ((ch = reader.Read()) != char.MinValue && Protocol.IsWhitespace(ch));
            
            text.StartIndex = reader.Position;
            return text;  // StringSegment is a struct, so this will return a copy
        }
        
        public static int FindChar(StringSegment text, char ch)
        {
            return FindChar(text, ch, false);
        }
        
        public static int FindChar(StringSegment text, char ch, bool specialChar)
        {
            CharReader reader = new CharReader(text);
            while (reader.ReadTo(ch))
            {
                if (!specialChar || !reader.IsPrev(Protocol.Escape))
                {
                    return reader.Position;
                }
            }
            
            return -1;
        }
    }
}
